package com.inepex.inechart.client.linechart;


import java.util.ArrayList;
import java.util.Iterator;

import com.inepex.inechart.client.Defaults;
import com.inepex.inechart.client.shape.Shape;
import com.inepex.inechart.shared.linechart.Curve;
import com.inepex.inechart.shared.linechart.LineChartToGO;
import com.inepex.inechart.shared.linechart.Point;
import com.inepex.inechart.shared.properties.Color;
import com.inepex.inechart.shared.properties.LineProperties.LineStyle;
import com.inepex.inegraphics.impl.client.IncrementalProgress;
import com.inepex.inegraphics.shared.DrawingAreaAssist;
import com.inepex.inegraphics.shared.GraphicalObjectContainer;
import com.inepex.inegraphics.shared.gobjects.Path;
import com.inepex.inegraphics.shared.gobjects.PathElement;

public class LineChartToGoGWT extends LineChartToGO implements IncrementalProgress{

	public enum LineChartGOCreationType{
		fillPathCalc,
		strokePathCalc,
		curveToGO
	}

	protected static final int minimumPointProcessPerUpdatePart = 6;

	protected Curve curve;
	protected LineChartGOCreationType actualJob;
	protected LineChartGwt lineChartGwt;
	protected ArrayList<Point> points;
	protected int totalPointCount;
	protected int processedPointCount;
	protected long updatePartStart;

	protected Path fillPath;
	protected Path strokePath;
	protected Shape pointShape;
	protected IncrementalProgressCaller updater;
	protected double yFrom, yTo, xFrom, xTo;

	protected GraphicalObjectContainer lineChartGOs;
	protected GraphicalObjectContainer pointChartGOs;

	protected double[] lastPair = null;
	protected double[] lastLineEnd = null;

	public LineChartToGoGWT(LineChartGwt lineChartGwt, Curve curve) {
		super(lineChartGwt.getCartesianAssist());
		this.lineChartGwt = lineChartGwt;
		this.curve = curve;
		points = new ArrayList<Point>();
		lineChartGOs = new GraphicalObjectContainer();
		pointChartGOs = new GraphicalObjectContainer();
	}

	protected boolean shouldCalculateFillPathForCurve(Curve curve){
		if( curve.isAutoFill()  ||
				(curve.getToCurveFills() != null && curve.getToCurveFills().size() > 0) || 
				(curve.getToYFills() != null && curve.getToYFills().size() > 0)){
			return true;
		}
		else{
			for(Curve c : lineChartGwt.curves){
				if(c == curve)
					continue;
				if(c.getToCurveFills() != null && c.getToCurveFills().containsKey(curve))
					return true;
			}
		}
		return false;
	}

	protected boolean canContinue(int actualPointCount){
		return actualPointCount < minimumPointProcessPerUpdatePart || System.currentTimeMillis() - updatePartStart < updater.getProgressPartTresholdMS();
	}

	protected void calcFillPathForCurve(){
		Iterator<Point> pointIterator = points.iterator();
		int actualPointCount = 0;
		while(pointIterator.hasNext() && canContinue(actualPointCount)){
			Point dataPoint = pointIterator.next();
			double[] actual = projectPointToViewport(dataPoint, xFrom, xTo, yFrom, yTo);
			if(fillPath == null){
				appendPointOrCreatePath(actual, fillPath);
			}
			else{
				double[] intersection = DrawingAreaAssist.getIntersection(
						lastPair[0], lastPair[1],
						dataPoint.getCanvasX(), dataPoint.getCanvasY(),
						xFrom,
						yFrom,
						xTo - xFrom,
						yTo - yFrom);
				if(intersection != null){
					if(lastLineEnd[0] != intersection[0] || lastLineEnd[1] != intersection[1]){
						fillPath.lineTo(intersection[0], intersection[1], false);
					}
					fillPath.lineTo(intersection[2], intersection[3], false);
				}
				appendPointOrCreatePath(actual, fillPath);
			}
			lastPair = new double[]{dataPoint.getCanvasX(), dataPoint.getCanvasY()};
			lastLineEnd = actual;
			pointIterator.remove();
			actualPointCount++;
		}
		processedPointCount += actualPointCount;
	}

	protected void calcStrokePathForCurve(){
		Iterator<Point> pointIterator = points.iterator();
		int actualPointCount = 0;
		while(pointIterator.hasNext() && canContinue(actualPointCount)){
			Point dataPoint = pointIterator.next();
			if(getDiscontinuitiesAsPoints(curve).contains(dataPoint)){
				lastPair = null;
			}
			else{
				if(lastPair != null){
					double[] intersection = DrawingAreaAssist.getIntersection(
							lastPair[0], lastPair[1],
							dataPoint.getCanvasX(), dataPoint.getCanvasY(),
							xFrom,
							yFrom,
							xTo - xFrom,
							yTo - yFrom);
					if(intersection != null){
						if(strokePath == null){
							strokePath = new Path(intersection[0], intersection[1], true, false, curve.getZIndex(), null);
						}
						else if(lastLineEnd[0] != intersection[0] || lastLineEnd[1] != intersection[1]){
							strokePath.moveTo(intersection[0], intersection[1], false);
						}
						strokePath.lineTo(intersection[2], intersection[3], false);
						lastLineEnd = new double[]{intersection[2], intersection[3]};
					}
				}
				lastPair = new double[]{dataPoint.getCanvasX(), dataPoint.getCanvasY()};
			}
			pointIterator.remove();
			actualPointCount++;
		}
		processedPointCount += actualPointCount;
	}

	protected void createPointChartGOs(){
		Iterator<Point> pointIterator = points.iterator();
		int actualPointCount = 0;
		while(pointIterator.hasNext() && canContinue(actualPointCount)){
			Point dataPoint = pointIterator.next();
			pointChartGOs.add(createPoint(curve, dataPoint, pointShape));
			pointIterator.remove();
			actualPointCount++;
		}
		processedPointCount += actualPointCount;
	}

	protected void createLineChartGOs(){
		/* stroke path - the curve's line */
		if (curve.hasLine() && strokePath != null) {
			Path line = new Path(strokePath);
			line.setContext(createLineContext(curve));
			line.setStroke(true);
			line.setFill(false);
			if (curve.getLineProperties() != null && curve.getLineProperties().getStyle().equals(LineStyle.DASHED)){
				lineChartGOs.add(DrawingAreaAssist.createDashedLine(
						line,
						curve.getLineProperties().getDashStrokeLength(), 
						curve.getLineProperties().getDashDistance()));
			}
			else{
				lineChartGOs.add(line);
			}
		}
		/* fill paths - the curve's fill GOs */
		if(fillPath != null){
			/* autofill property (fills below the curve) */ //TODO vertical curve
			if (curve.isAutoFill()) {
				Path autoFill = new Path(fillPath);
				autoFill.lineTo(autoFill.getLastPathElement().getEndPointX(), yTo, false);
				autoFill.lineTo(autoFill.getBasePointX(), yTo, false);
				autoFill.lineToBasePoint();
				autoFill.setFill(true);
				autoFill.setStroke(false);
				Color c;
				if (curve.getLineProperties() != null){
					c = curve.getLineProperties().getLineColor();
				}
				else {
					c = curve.getDataSet().getColor();
				}
				autoFill.setContext(createFillContext(c));
				autoFill.getContext().setAlpha(Defaults.fillOpacity);
				lineChartGOs.add(autoFill);
			}
			if (curve.getToCurveFills() != null && curve.getToCurveFills().size() > 0) {
				for (Curve toCurve : curve.getToCurveFills().keySet()) {
					Path fillToCurve = new Path(
							fillPath.getBasePointX(),
							fillPath.getBasePointY(), 
							false,
							true,
							curve.getZIndex(), createFillContext(curve.getToCurveFills().get(toCurve)));
					Path otherCurvesFillPath = lineChartGwt.curveToGOMap.get(toCurve).fillPath;
					if (otherCurvesFillPath != null){
						for (PathElement e : fillPath.getPathElements()) {
							fillToCurve.lineTo(e.getEndPointX(), e.getEndPointY(), false);
						}
						for (int i = otherCurvesFillPath.getPathElements().size() - 1; i >= 0; i--) {
							PathElement e = otherCurvesFillPath.getPathElements().get(i);
							fillToCurve.lineTo(e.getEndPointX(), e.getEndPointY(), false);
						}
						fillToCurve.lineTo(otherCurvesFillPath.getBasePointX(),
								otherCurvesFillPath.getBasePointY(), false);
						fillToCurve.lineToBasePoint();
						lineChartGOs.add(fillToCurve);
					}
				}
			}
			if (curve.getToYFills() != null && curve.getToYFills().size() > 0) {
				//TODO axis direction!!
				for (double y : curve.getToYFills().keySet()) {
					Path fillToY = new Path(fillPath);
					double calculatedY =  cartesianAssist.modelToCanvasVertical(y);
					fillToY.lineTo(fillToY.getLastPathElement().getEndPointX(),calculatedY, false);
					fillToY.lineTo(fillToY.getBasePointX(), calculatedY, false);
					fillToY.lineToBasePoint();
					fillToY.setFill(true);
					fillToY.setzIndex(curve.getZIndex());
					fillToY.setContext(createFillContext(curve.getToYFills().get(y)));
					lineChartGOs.add(fillToY);
				}
			}
		}
	}

	protected void appendPointOrCreatePath(double[] point, Path path){
		if(path == null){
			path = new Path(point[0], point[1], false, true, 0, null);
		}
		else{
			path.lineTo(point[0], point[1], false);
		}
	}

	protected double[] projectPointToViewport(Point point, double xFrom, double xTo, double yFrom, double yTo){
		double pX = Math.min(Math.max(point.getCanvasX(), xFrom), xTo);
		double pY = Math.min(Math.max(point.getCanvasY(), yFrom), yTo);
		return new double[]{pX,pY};
	}

	public void initJob(LineChartGOCreationType actualJob, CurvePart curvePart, double xFrom, double xTo, double yFrom, double yTo){
		this.actualJob = actualJob;
		this.xFrom = xFrom;
		this.xTo = xTo;
		this.yFrom = yFrom;
		this.yTo = yTo;
		switch (actualJob) {
		case fillPathCalc:
			fillPath = null;
			setAcutalPoints(curvePart, true, true);
			lastLineEnd = null;
			lastPair = null;
			break;
		case strokePathCalc:
			strokePath = null;
			setAcutalPoints(curvePart, false, true);
			lastLineEnd = null;
			lastPair = null;
			break;
		case curveToGO:
			lineChartGOs.removeAllGraphicalObjects();
			pointChartGOs.removeAllGraphicalObjects();
			if(curve.hasPoint()){
				setAcutalPoints(curvePart, false, false);
				pointShape = curve.getPointShape();
				if(pointShape == null){
					//use defaults
					pointShape = Defaults.normalPoint();	
					pointShape.getProperties().getLineProperties().setLineColor(curve.getDataSet().getColor());
				}
				pointShape.setZIndex(curve.getZIndex());
				if(curve.hasShadow()){
					if(pointShape != null){
						pointShape.setShadowColor(curve.getShadowColor());
						pointShape.setShadowOffsetX(curve.getShadowOffsetX());
						pointShape.setShadowOffsetY(curve.getShadowOffsetY());
					}
				}
			}
			else{
				points.clear();
			}
			createLineChartGOs();
			break;
		}
		totalPointCount = points.size();
		processedPointCount = 0;
	}

	protected void setAcutalPoints(CurvePart curvePart, boolean includeExtremes, boolean includeJustNexts){
		points.clear();
		if(includeExtremes){
			if(curvePart.getBeforeMin() != null){
				points.add(curvePart.getBeforeMin());
			}
			if(curvePart.getBeforeMax() != null){
				points.add(curvePart.getBeforeMax());
			}
		}
		if(includeJustNexts && curvePart.getJustBefore() != null){
			points.add(curvePart.getJustBefore());
		}
		points.addAll(curvePart.getInsidePoints());
		if(includeJustNexts && curvePart.getJustAfter() != null){
			points.add(curvePart.getJustAfter());
		}
		if(includeExtremes){
			if(curvePart.getAfterMin() != null){
				points.add(curvePart.getAfterMin());
			}
			if(curvePart.getAfterMax() != null){
				points.add(curvePart.getAfterMax());
			}
		}
	}

	@Override
	public void initProgress(IncrementalProgressCaller updater) {
		this.updater = updater;
	}

	@Override
	public boolean nextProgressPart() {
		if(actualJob == null){
			return false;
		}
		updatePartStart = System.currentTimeMillis();
		switch (actualJob) {
		case curveToGO:
			createPointChartGOs();
			break;
		case fillPathCalc:
			calcFillPathForCurve();
			break;
		case strokePathCalc:
			calcStrokePathForCurve();
			break;
		}
		return processedPointCount < totalPointCount;
	}

	@Override
	public void cancelProgress() {
		actualJob = null;
	}

	@Override
	public double getProgressRate() {
		return processedPointCount / totalPointCount;
	}

}
